const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
// const types = require('@babel/types');
const generator = require('@babel/generator').default;
const {SyncHook} = require('tapable');

class Compiler {
    constructor(config) {
        const {entry} = config;
        this.entry = entry;
        this.config = config;
        this.modules = {};
        this.content = null;
        this.isAsync = false;
        this.hooks = {
            entryOption: new SyncHook(),
            compile: new SyncHook(),
            afterCompile: new SyncHook(),
            afterPlugins: new SyncHook(),
            run: new SyncHook(),
            emit: new SyncHook(),
            done: new SyncHook()
        };
        this.savePluginInFnToHooks();
        this.hooks.afterPlugins.call();
    }

    changeSlash(path) {
        if (!path) {
            return path;
        }
        path = path.replace(/\\/g, '/');
        return path;
    }

    parseLoaderConfig(config) {
        if (typeof config === 'object') {
            return config;
        } else {
            return {loader: config, options: null}
        }
    }

    isMatchLoader(rules = [], modulePath) {
        return rules.find((rule) => {
            const {test, use} = rule;
            return test.test(modulePath);
        })
    }

    parseFileByLoader(modulePath, content) {
        return new Promise((resolve, reject) => {
            const {rules} = this.config.module;
            this.loaders = rules;
            const rule = this.isMatchLoader(rules, modulePath);
            if (!(rules && rules.length) || !rule) {
                resolve(content);
            }
            const {use} = rule;
            const next = (val) => {
                if (use.length === 0) {
                    resolve(val);
                    return;
                }
                const loaderConfig = use.pop();
                const {loader, options} = this.parseLoaderConfig(loaderConfig);
                this.query = options;
                let fnLoader = require(loader);
                let ret = fnLoader.call(this, val) || '';
                ret = content = this.content || ret;
                this.query = null;
                !this.isAsync && next(ret);
            }
            this.callback = (err, content, sourcemap, ast) => {
                this.content = content;
                if (this.isAsync) {
                    this.isAsync = false;
                    next(content);
                }
            };
            this.async = () => {
                this.isAsync = true;
                return this.callback;
            };
            next(content);
        });
    }

    savePluginInFnToHooks() {
        const {plugins} = this.config;
        if (Array.isArray(plugins)) {
            plugins.forEach((plugin) => {
                plugin.apply(this);
            });
        }
    }

    async parse(modulePath, isEntry) {
        return new Promise( async (resolve, reject) => {
            const dirname = path.dirname(modulePath);
            let content = fs.readFileSync(modulePath, 'utf-8');
            content = await this.parseFileByLoader(modulePath, content);
            const ast = babylon.parse(content);
            const self = this;
            const dependence = []
            traverse(ast, {
                CallExpression(p) {
                    const node = p.node;
                    if (node.callee.name === 'require') {
                        node.callee.name = '__webpack_require__';
                        let moduleName = node.arguments[0].value;
                        moduleName = self.changeSlash('./' + path.join(dirname, moduleName));
                        dependence.push(moduleName);
                        node.arguments = node.arguments.map((item) => {
                            item.value = moduleName;
                            return item;
                        });
                        // node.arguments = [types.stringLiteral(moduleName)];
                    }
                }
            });
            this.modules[modulePath] = generator(ast, {
                minified: true
            }).code;
            while(dependence.length) {
                await this.parse(dependence.shift(), false);
            }
            resolve(this.modules);
        })
    }

    emitFile() {
        let content = this.getFileContent();
        const {filename} = this.config.output;
        const writePath = path.resolve(this.config.output.path, filename);
        fs.writeFileSync(writePath, content);
    }

    getFileContent() {
        const list = [];
        const modules = this.modules;
        for (const key in modules) {
            list.push(`'${key}':
            (function(module, exports, __webpack_require__) {
                eval('${modules[key]}');
            })`);
        }
        return `
            (function() {
                var __webpack_modules__ = ({
                    ${list.join(',\n')}
                });
                var __webpack_module_cache__ = {};
                
                function __webpack_require_(moduleId) {
                    if (__webpack_module_cache__[moduleId]) {
                        return __webpack_module_cache__[moduleId];
                    }
                    var module = __webpack_module_cache__[moduleId] = {
                        exports: {}
                    }
                    __webpack_modules__[moduleId](module, module.exports, __webpack_require_);
                    return module.exports;
                }
                
                var __webpack_exports__ = __webpack_require_('${this.entry}');
            })();
        `;
    }

    async run() {
        this.hooks.compile.call();
        // 1.解析模块
        const res = await this.parse(this.entry, true);
        console.log('res', res);
        this.hooks.afterCompile.call();
        // 2.发射文件
        this.emitFile();
        this.hooks.emit.call();
        this.hooks.done.call();
    }
}

module.exports = Compiler;